Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ cannot be given it, and GemDB Code says so rather than starting it.
`gemdb app.py`, or of **Run Python File in GemDB**, stop the program as Ctrl+C would. On Linux it
usually did not: the program carried on out of sight, its `finally` blocks unrun, holding its
database session. macOS was not affected.
- **The window and the GemDB Shell wait for an external database that is still starting.** They
used to report one at `Startup` as up, and the first login then failed. A stone that is shutting
down is now said to be run by the machine's administrator, who is the one to start it again.
- **`gemdb file.py`, `-c` and `-m` start the database the way the window and the GemDB Shell do.**
A database left by an older engine is now refused with the sentence that says what to delete,
instead of a raw 4045 at the first login, and a lack of room or an NFS root path is refused
too. A `gemdb` run that finds the database being started by another process now
says it is waiting, and waits as long as the window does, instead of giving up after ten seconds.
A run starts the database only, not the session listener it never uses. A mistyped file name or
a `-c` with no code no longer starts the database before failing.
- **`gemdb` says so when Python has not been installed into the database yet.** Run on a database
GemDB Code had not finished setting up, a file, `-c`, `-m` and the GemDB Shell printed compile
errors from the driver, or a refused login, and the GemDB Shell in a terminal could start a
database with no Python in it. They now stop with "Python support is not installed in this
database yet. Run "GemDB: Start GemDB" in VS Code and let it finish", before starting anything.
- **A line or block pasted into the Debug Console with leading spaces runs**, and so does a watch
expression written that way. It used to fail with `IndentationError: unexpected indent`; the
indentation every line shares is now taken off before it runs, as Python's `textwrap.dedent`
Expand Down
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ force-stop uses), and "ready" means gslist status `OK`, not "listed": a
stone at `Startup` is listed and refuses logins, so wait on it, never start it.
`stoneState` reads the rest, case-insensitively since the engine prints mixed
case: `Shutdown` fails at once, and any other listed status (`exe deleted`,
`frozen`, ...) goes on to the login as it always did. The wrapper in `cli.ts`
makes the same calls in shell; change both together.
`frozen`, ...) goes on to the login as it always did. The `gemdb` wrapper in
`cli.ts` keeps no copy of this: it only fast-paths a stone that reads `ok`, and
every other case (including an external database) runs
`gemdb-shell.js --ensure-stone`, which calls `bringUp` (`cliStart.ts`) and so
`ensureProcesses` with `assertSafeToStart`. File mode starts the stone only
(`listener: false`): it is a linked topaz and never goes through NetLDI.

## CI

Expand Down Expand Up @@ -219,7 +223,10 @@ bundle and `node_modules/koffi` (this platform's binary only) to
`process.execPath` recorded at generation time, `ELECTRON_RUN_AS_NODE=1`, a
PATH `node` as fallback. "Open GemDB Shell" opens a terminal on that wrapper,
so the shell is out of the extension host entirely: a wedged FFI call is a
dead tab, not a dead window. The reason this exists is measured topaz history:
dead tab, not a dead window. The same bundle is the wrapper's cold start:
`--ensure-stone` runs before anything touches stdin, so a file, `-c` or `-m`
run that finds the stone down needs the bundle too (and refuses without it,
with the Shell's messages, rather than falling back to bash). The reason this exists is measured topaz history:
Grail's own topaz REPL (`gemdb`'s old no-argument handoff) wraps evaluation in
`on: Error do:`, and Grail's Python exceptions descend from
`AbstractException`, not `Error` — so a `ZeroDivisionError` printed a
Expand Down
46 changes: 44 additions & 2 deletions src/__integration__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import * as os from 'os';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { cliPath } from '../cli';
import { bundledGrailStamp, stageGrail } from '../grail';
import { isRunning, stopStone } from '../processes';
import { bundledGrailStamp, recordGrailInstalled, stageGrail } from '../grail';
import { GRAIL_NOT_INSTALLED } from '../grailMessages';
import { grailStampPath } from '../paths';
import { isListening, isRunning, stopStone } from '../processes';
import {
createDatabaseWithPython,
Fixture,
Expand Down Expand Up @@ -75,6 +77,9 @@ beforeAll(async () => {

createDatabaseWithPython(fixture); // Python already filed in — see testExtentPath
stageGrail(ext); // stages Grail and generates the CLI
// The extent already has Python; the stamp is what the wrapper checks, and
// staging deleted it with the rest of grail/.
recordGrailInstalled(ext);
await startStoneAlone();

fs.writeFileSync(path.join(workDir, 'hello.py'), 'print("hello from", 6 * 7)\n');
Expand Down Expand Up @@ -414,6 +419,43 @@ close
expect(ran.stdout).toContain('back');
expect(ran.code).toBe(0);
expect(isRunning()).toBe(true);
// A file run is a linked topaz: the listener is none of its business.
expect(isListening()).toBe(false);
});
});

describe.skipIf(!ready || !canMakeFixture())('the gemdb command without Python', () => {
let aside: string;

beforeAll(() => {
aside = `${grailStampPath()}.aside`;
fs.renameSync(grailStampPath(), aside);
});

afterAll(() => {
fs.renameSync(aside, grailStampPath());
});

it.each([
['a file', ['hello.py']],
['-c', ['-c', 'print(1)']],
['-m', ['-m', 'json.tool']],
])('refuses %s, saying where Python comes from, on a running stone', async (_mode, args) => {
const ran = await gemdb(...args);

expect(ran.code).toBe(1);
expect(ran.stderr).toBe(`gemdb: ${GRAIL_NOT_INSTALLED}\n`);
expect(ran.stdout).toBe('');
});

it('refuses before starting a stone that is down', async () => {
await stopStone(false);

const ran = await gemdb('hello.py');

expect(ran.code).toBe(1);
expect(ran.stderr).toBe(`gemdb: ${GRAIL_NOT_INSTALLED}\n`);
expect(isRunning()).toBe(false);
});
});

Expand Down
20 changes: 19 additions & 1 deletion src/__integration__/repl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import * as fs from 'fs';
import * as path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { cliPath } from '../cli';
import { bundledGrailStamp, stageGrail } from '../grail';
import { bundledGrailStamp, recordGrailInstalled, stageGrail } from '../grail';
import { GRAIL_NOT_INSTALLED } from '../grailMessages';
import { grailStampPath } from '../paths';
import { isRunning, stopNetldi, stopStone } from '../processes';
import { createDatabaseWithPython, Fixture, haveTestExtent, makeFixture } from './fixture';

Expand Down Expand Up @@ -83,6 +85,8 @@ beforeAll(() => {
if (!fixture) return;
createDatabaseWithPython(fixture); // Python already filed in — see testExtentPath
stageGrail(ext); // stages Grail, the CLI, and the shell bundle
// The extent already has Python; staging deleted the stamp with grail/.
recordGrailInstalled(ext);
});

afterAll(async () => {
Expand Down Expand Up @@ -175,6 +179,20 @@ send "exit()\\r"
expect(ran.code).toBe(0);
expect(isRunning()).toBe(true); // the shell brought the stone up itself
});

it('refuses to open without Python in the database, even on a running stone', async () => {
const aside = `${grailStampPath()}.aside`;
fs.renameSync(grailStampPath(), aside);
try {
const ran = await driveShell('');

expect(ran.code).toBe(1);
expect(ran.transcript).toContain(`gemdb: ${GRAIL_NOT_INSTALLED}`);
expect(ran.transcript).not.toContain('>>> ');
} finally {
fs.renameSync(aside, grailStampPath());
}
});
});

/** See database.test.ts — skipIf is evaluated during collection. */
Expand Down
Loading
Loading