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
5 changes: 5 additions & 0 deletions packages/stim-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ stim logs --errors
stim stop
```

For `stim logs --errors`, a clean check requires exit code 0 and no matching
errors in the captured logs. Exit code 0 alone means the query succeeded, even
when it prints errors; an empty result does not prove launch or log capture
succeeded.

Use `stim doctor --platform ios` or `stim doctor --platform android` when only
one native platform is in scope; shared project checks still run.

Expand Down
16 changes: 15 additions & 1 deletion packages/stim-cli/src/__tests__/guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,9 @@ test('the agent guide carries the normal workflow and safety rules', () => {
expect(agent).toMatch(/give the user one compact result: exact device,[\s\S]*total duration/);
expect(agent).toMatch(/whether stim logs\s+--errors passed/);
expect(agent).toMatch(/Do not repeat the\s+phase transcript/);
expect(agent).toMatch(/Exit code 0 from logs --errors is the pass condition/);
expect(agent).toMatch(/requires exit code 0 AND no matching errors in\s+captured logs/);
expect(agent).toMatch(/Exit code 0 alone means the query succeeded, even when errors\s+were printed/);
expect(agent).toMatch(/does not prove launch or log\s+capture succeeded/);
expect(agent).toContain('No matching log records');
expect(agent).toContain('stim reload');
expect(normalWorkflow).not.toContain('stim reload');
Expand All @@ -1035,6 +1037,18 @@ test('the agent guide carries the normal workflow and safety rules', () => {
expect(agent).toMatch(/Ordinary stim stop and an authorized clean\s+stim worktree remove do not need/);
});

test('the logs and lifecycle guides distinguish query success from a clean captured timeline', () => {
const logs = renderTopic('logs');
assert(logs);
expect(logs).toContain('EXIT 0 MEANS THE QUERY SUCCEEDED, whether or not records matched');
expect(logs).toMatch(/requires exit code 0 AND no matching errors in\s+captured logs/);
expect(logs).toContain('a workspace with no log directory also returns an empty result');
const lifecycle = renderTopic('lifecycle');
assert(lifecycle);
expect(lifecycle).toContain('Check captured errors: require exit 0 AND no matching errors');
expect(lifecycle).toContain('Exit 0 alone means the query succeeded, even when it printed errors');
});

test('the logs guide keeps Expo and bare React Native stack context on one human error record', () => {
const logs = renderTopic('logs');
assert(logs);
Expand Down
22 changes: 14 additions & 8 deletions packages/stim-cli/src/commands/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ RULES DURING THE LOOP
still building" means Metro has not finished; wait and query the logs. For
launch UNVERIFIED, follow the printed remedy before claiming success. JSON
reports these as true, "bundling", and "unverified" in launched.
- Exit code 0 from logs --errors is the pass condition. Human output can show
"No matching log records" on stderr. JSON mode prints zero bytes when no
records match. Do not read the NDJSON files directly.
- A clean logs --errors check requires exit code 0 AND no matching errors in
captured logs. Exit code 0 alone means the query succeeded, even when errors
were printed. Human output shows "No matching log records" on stderr for
zero matches; JSON mode prints zero bytes. This does not prove launch or log
capture succeeded. Do not read the NDJSON files directly.
- Use stim status when resuming a workspace or recovering missing device,
port, server, or build facts. A normal start and platform run already print
them. Use stim doctor when a build is unexpectedly slow or the environment
Expand Down Expand Up @@ -660,9 +662,12 @@ Reads every *.ndjson file in the global workspace logs directory, merges them in
ordered by timestamp, prints what matches, and EXITS. The file set is
discovered, not enumerated.

NOTHING MATCHING IS EXIT 0. \`stim logs --errors\` finding nothing is the
pass condition of a build loop, so an empty result must never read as a
failure. Precisely what that looks like: STDOUT IS EMPTY, exit code 0, and
EXIT 0 MEANS THE QUERY SUCCEEDED, whether or not records matched. A clean
\`stim logs --errors\` check requires exit code 0 AND no matching errors in
captured logs. An empty result does not prove launch or log capture succeeded;
a workspace with no log directory also returns an empty result.

For zero matches: STDOUT IS EMPTY, exit code 0, and
one dim note on STDERR reading \`No matching log records in <logs dir>\`
(human mode only -- \`--json\` prints nothing at all, on either stream).
The only exit-1 paths are a malformed query and no project.
Expand Down Expand Up @@ -1719,8 +1724,9 @@ STIM_CONFIG_CORRUPT ("Stim config at <path> is not valid JSON")
install from cache (3s)
launch com.example.app (1s)

# 4. Did it work? Exit 0 is the pass condition. Human mode prints a
# "No matching log records" note on stderr when the result is clean.
# 4. Check captured errors: require exit 0 AND no matching errors.
# Human mode prints "No matching log records" on stderr for zero matches.
# Exit 0 alone means the query succeeded, even when it printed errors.
stim logs --errors

# 5. Edit the JS. Fast Refresh applies it; no Stim command is involved.
Expand Down
80 changes: 80 additions & 0 deletions test/e2e/logs.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { afterEach, beforeEach, test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { workspaceLogsDir } from '../../packages/stim-cli/src/paths.ts';

const CLI = fileURLToPath(new URL('../../packages/stim-cli/bin/cli.ts', import.meta.url));
let home;
let project;

beforeEach(() => {
home = realpathSync(mkdtempSync(join(tmpdir(), 'stim-logs-e2e-home-')));
process.env.STIM_HOME = home;
project = realpathSync(mkdtempSync(join(tmpdir(), 'stim-logs-e2e-project-')));
writeFileSync(join(project, 'package.json'), '{"name":"logs-fixture","private":true}\n');
});

afterEach(() => {
rmSync(home, { recursive: true, force: true });
rmSync(project, { recursive: true, force: true });
delete process.env.STIM_HOME;
});

test('an empty error query exits 0, with a human-only note on stderr', () => {
writeLogs([{ ts: 1, src: 'metro', level: 'info', msg: 'bundling' }]);
const human = run();
assert.equal(human.status, 0, human.stderr);
assert.equal(human.stdout, '');
assert.match(human.stderr, /No matching log records/);

const json = run(['--json']);
assert.equal(json.status, 0, json.stderr);
assert.equal(json.stdout, '');
assert.equal(json.stderr, '');
});

test('matching errors also exit 0, so a successful query alone is not a clean check', () => {
const error = { ts: 2, src: 'metro', level: 'error', msg: 'App failed to render' };
writeLogs([{ ts: 1, src: 'metro', level: 'info', msg: 'bundle completed', marker: true }, error]);
const human = run();
assert.equal(human.status, 0, human.stderr);
assert.match(human.stdout, /App failed to render/);
assert.doesNotMatch(human.stderr, /No matching log records/);

const json = run(['--json']);
assert.equal(json.status, 0, json.stderr);
assert.deepEqual(json.stdout.trim().split('\n').map(JSON.parse), [error]);
assert.equal(json.stderr, '');
});

test('no log directory is a successful empty query without evidence of capture', () => {
const result = run(['--json']);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, '');
assert.equal(result.stderr, '');
});

test('an invalid query exits 1 with empty stdout and a diagnostic on stderr', () => {
const result = run(['--json', '--source', 'unknown']);
assert.equal(result.status, 1);
assert.equal(result.stdout, '');
assert.match(result.stderr, /Unknown --source value/);
});

function writeLogs(records) {
const dir = workspaceLogsDir(project);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'metro.ndjson'), records.map((record) => `${JSON.stringify(record)}\n`).join(''));
}

function run(args = []) {
return spawnSync(process.execPath, [CLI, 'logs', '--errors', ...args], {
cwd: project,
env: process.env,
encoding: 'utf-8',
});
}
8 changes: 6 additions & 2 deletions website/docs/dev-server-and-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ The merged timeline includes Metro, client, device, and build records. Logs live
in the global workspace directory under `$STIM_HOME/workspaces`, not in the
project checkout.

No matching records is a successful result with exit code 0. JSON mode writes
NDJSON and writes zero bytes for zero matches.
Exit code 0 means the query succeeded, including when it prints errors. A clean
`stim logs --errors` check requires exit code 0 and no matching errors in the
captured logs. Human mode prints `No matching log records` on stderr for zero
matches; JSON mode writes NDJSON and writes zero bytes for zero matches. An
empty result does not prove launch or log capture succeeded: a workspace with
no log directory also returns an empty result.

`stim stop` ends the supervisor and log collectors. It also frees the reserved
port and shuts down the owned local device.
2 changes: 1 addition & 1 deletion website/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The agent normally runs:
code={`stim doctor # inspect the main checkout and warm-state gaps
stim start # start this workspace's dev server
stim ios # build or restore, install, launch, and verify
stim logs --errors # no matching records means the launch is healthy
stim logs --errors # check for errors in the captured logs
stim stop # release the live environment`}
/>

Expand Down
Loading