claude-code-session-manager 0.95.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DataModel-DZK9QZpV.js → DataModel-T0D00BpK.js} +1 -1
- package/dist/assets/History-DuAovTr7.js +2 -0
- package/dist/assets/{Hooks-CmK6l0uf.js → Hooks-CgP9jsAN.js} +3 -3
- package/dist/assets/{HostBilko-D8TIx-Bx.js → HostBilko-DHfz3Mxa.js} +1 -1
- package/dist/assets/{Library-DTxpOjx8.js → Library--poVP-d1.js} +1 -1
- package/dist/assets/{MarkdownEditor-BPJyrevq.js → MarkdownEditor-C56-Pc4c.js} +1 -1
- package/dist/assets/{McpServers-B_rvIYDs.js → McpServers-D-ykP4Vi.js} +2 -2
- package/dist/assets/Memory-BrkJjRL8.js +8 -0
- package/dist/assets/{Permissions-DhMMp27n.js → Permissions-BMpAaxJu.js} +3 -3
- package/dist/assets/Plugins-OuEe5S_T.js +2 -0
- package/dist/assets/{ProvenanceBadge-ptlItdCK.js → ProvenanceBadge-4eNbDYe8.js} +1 -1
- package/dist/assets/{SaveBar-CSbNSl2W.js → SaveBar-BQ8y9Lkv.js} +1 -1
- package/dist/assets/Scheduler-DcMT5sT_.js +16 -0
- package/dist/assets/{ScopeSwitcher-BJof-GWH.js → ScopeSwitcher-XdxtVKP-.js} +1 -1
- package/dist/assets/Settings-FdaPUq2t.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CGl4B89-.js → SkillReferenceGraph-CJzvvc0_.js} +1 -1
- package/dist/assets/Skills-Wvm9OE1e.js +3 -0
- package/dist/assets/{SystemPrompt-DKj-q39J.js → SystemPrompt--LYw_69j.js} +1 -1
- package/dist/assets/TagLibrary-Be6HbMMx.js +1 -0
- package/dist/assets/{TiptapBody-BbcJL9az.js → TiptapBody-DzFlWMgO.js} +1 -1
- package/dist/assets/{Toggle-DAslvnnv.js → Toggle-BxtAFE1i.js} +1 -1
- package/dist/assets/index-XkrLB67D.js +3079 -0
- package/dist/assets/{settingsSchema-pzp0TgvN.js → settingsSchema-BIvDZ1Cj.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +8 -2
- package/plugins/CLAUDE.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +83 -67
- package/plugins/session-manager-dev/skills/develop/standards.md +5 -11
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +5 -15
- package/scripts/README.md +1 -0
- package/scripts/hooks/guard-destructive-git.cjs +9 -374
- package/scripts/hooks/guard-inline-implementation.cjs +9 -128
- package/scripts/hooks/guard-prd-writes.cjs +9 -74
- package/scripts/hooks/guard-self-schedule.cjs +9 -61
- package/scripts/hooks/lib/guard-destructive-git-policy.cjs +410 -0
- package/scripts/hooks/lib/guard-inline-implementation-policy.cjs +159 -0
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +100 -0
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +89 -0
- package/scripts/scheduler-mcp-server.cjs +8 -1
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +59 -0
- package/src/main/__tests__/exchanges.test.cjs +41 -96
- package/src/main/__tests__/exchangesPromptId.test.cjs +16 -25
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/health-credentials.test.cjs +81 -0
- package/src/main/__tests__/memoryAggregate.test.cjs +40 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +1 -1
- package/src/main/__tests__/planValidator.test.cjs +71 -0
- package/src/main/__tests__/prdCreate.test.cjs +3 -1
- package/src/main/__tests__/prdSizing.test.cjs +106 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +46 -0
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +10 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +40 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +81 -0
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +6 -1
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +110 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +19 -1
- package/src/main/__tests__/seedValidatorPersona.test.cjs +116 -0
- package/src/main/__tests__/telemetryClient.test.cjs +2 -2
- package/src/main/__tests__/telemetryContract.test.cjs +2 -2
- package/src/main/__tests__/usageSingleFlight.test.cjs +10 -0
- package/src/main/__tests__/validationSentinels.test.cjs +84 -0
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +26 -14
- package/src/main/exchanges.cjs +26 -21
- package/src/main/health.cjs +66 -4
- package/src/main/index.cjs +1 -1
- package/src/main/ipcSchemas.cjs +5 -1
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +1 -1
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +115 -0
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +85 -27
- package/src/main/lib/__tests__/gitWorktree.test.cjs +21 -27
- package/src/main/lib/__tests__/guardShims.test.cjs +10 -3
- package/src/main/lib/__tests__/loadGate.test.cjs +44 -1
- package/src/main/lib/__tests__/opsOwnership.test.cjs +14 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +4 -4
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +2 -2
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +1 -1
- package/src/main/lib/agentModelResolve.cjs +1 -0
- package/src/main/lib/credentials.cjs +62 -0
- package/src/main/lib/cwdClassify.cjs +15 -0
- package/src/main/lib/definitionOfDone.cjs +9 -13
- package/src/main/lib/effectiveModelInfo.cjs +2 -9
- package/src/main/lib/epicWorktreeProjectConfig.cjs +75 -31
- package/src/main/lib/loadGate.cjs +29 -5
- package/src/main/lib/opsOwnership.cjs +23 -0
- package/src/main/lib/planValidator.cjs +28 -0
- package/src/main/lib/prdCreate.cjs +8 -4
- package/src/main/lib/prdSizing.cjs +83 -0
- package/src/main/lib/runLogRetention.cjs +2 -8
- package/src/main/lib/telemetryBacklog.cjs +9 -3
- package/src/main/lib/telemetryClient.cjs +12 -3
- package/src/main/lib/telemetrySettings.cjs +8 -2
- package/src/main/lib/validationSentinels.cjs +29 -0
- package/src/main/memoryAggregate.cjs +36 -11
- package/src/main/otelSettings.cjs +8 -2
- package/src/main/scheduler.cjs +215 -31
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/sessionsStore.cjs +8 -2
- package/src/main/supervisor.cjs +3 -3
- package/src/main/templates/PRD_AUTHORING.md +9 -9
- package/src/main/voiceSettings.cjs +8 -2
- package/src/preload/api.d.ts +3 -3
- package/src/preload/index.cjs +5 -5
- package/src/seed/agents/dev-lead.md +24 -15
- package/src/seed/agents/validator.md +33 -0
- package/dist/assets/History-BDyJUGtR.js +0 -2
- package/dist/assets/Memory-B-9GlHqe.js +0 -8
- package/dist/assets/Plugins-DKfAlmMY.js +0 -2
- package/dist/assets/Scheduler--rn1QBYc.js +0 -16
- package/dist/assets/Settings-Dsx6XZP5.js +0 -3
- package/dist/assets/Skills-D522Zfol.js +0 -3
- package/dist/assets/TagLibrary-BH7CG7-6.js +0 -1
- package/dist/assets/index-Yf8e7VCB.js +0 -3079
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { test } from 'vitest';
|
|
4
4
|
const assert = require('node:assert/strict');
|
|
5
|
-
const
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { parseClusters, aggregate, clustersCachePath } = require('../memoryAggregate.cjs');
|
|
9
|
+
const { opsPath } = require('../lib/opsOwnership.cjs');
|
|
6
10
|
|
|
7
11
|
test('parseClusters maps well-formed JSON to the result shape and routes unlisted slugs to orphans', () => {
|
|
8
12
|
const slugs = ['scheduler_concurrency_memory', 'no_schedule_self_e2e', 'unrelated_note'];
|
|
@@ -68,3 +72,38 @@ test('parseClusters drops link endpoints and members that reference nonexistent
|
|
|
68
72
|
assert.deepEqual(result.clusters[0].links, []);
|
|
69
73
|
assert.deepEqual(result.orphans, []);
|
|
70
74
|
});
|
|
75
|
+
|
|
76
|
+
test('clustersCachePath resolves under <cwd>/session-manager-operations/memory-clusters/clusters.json, one file per project', () => {
|
|
77
|
+
const cwd = '/home/u/Projects/demo';
|
|
78
|
+
assert.equal(clustersCachePath(cwd), opsPath(cwd, 'memory-clusters', 'clusters.json'));
|
|
79
|
+
assert.match(clustersCachePath(cwd), /\/session-manager-operations\/memory-clusters\/clusters\.json$/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('aggregate persists and reads back the per-project cache file at the new ops-owned location', async () => {
|
|
83
|
+
const cwd = fs.mkdtempSync(path.join(os.homedir(), '.sm-test-memory-clusters-'));
|
|
84
|
+
try {
|
|
85
|
+
const workspace = `test-ws-${Date.now()}`;
|
|
86
|
+
|
|
87
|
+
const written = await aggregate({ workspace, refresh: true, cwd });
|
|
88
|
+
assert.equal(written.cached, false);
|
|
89
|
+
assert.deepEqual(written.clusters, []);
|
|
90
|
+
assert.deepEqual(written.orphans, []);
|
|
91
|
+
|
|
92
|
+
const expectedPath = opsPath(cwd, 'memory-clusters', 'clusters.json');
|
|
93
|
+
assert.ok(fs.existsSync(expectedPath), `expected cache file at ${expectedPath}`);
|
|
94
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(expectedPath, 'utf8')).workspace, workspace);
|
|
95
|
+
|
|
96
|
+
const read = await aggregate({ workspace, refresh: false, cwd });
|
|
97
|
+
assert.equal(read.cached, true);
|
|
98
|
+
assert.equal(read.workspace, workspace);
|
|
99
|
+
} finally {
|
|
100
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('aggregate without a cwd skips the cache instead of throwing', async () => {
|
|
105
|
+
const workspace = `test-ws-nocwd-${Date.now()}`;
|
|
106
|
+
const result = await aggregate({ workspace, refresh: false });
|
|
107
|
+
assert.equal(result.cached, false);
|
|
108
|
+
assert.deepEqual(result.clusters, []);
|
|
109
|
+
});
|
|
@@ -137,7 +137,7 @@ test('a realistic error with an absolute path and a prompt-like string never rea
|
|
|
137
137
|
process.env.HOME = home;
|
|
138
138
|
// Explicit opt-in: overrides the test-environment no-op guard so this
|
|
139
139
|
// test's real telemetryClient call actually persists into an isolated dir.
|
|
140
|
-
process.env.SM_TELEMETRY_SPOOL = path.join(home, '.
|
|
140
|
+
process.env.SM_TELEMETRY_SPOOL = path.join(home, '.claude', 'session-manager');
|
|
141
141
|
|
|
142
142
|
for (const p of ['../lib/telemetryClient.cjs', '../config.cjs', '../lib/telemetrySettings.cjs', '../lib/machineProfile.cjs']) {
|
|
143
143
|
const resolved = require.resolve(p);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* planValidator.test.cjs — unit tests for hasDownstreamValidator (PRD 1408).
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/planValidator.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect } from 'vitest';
|
|
10
|
+
const { hasDownstreamValidator } = require('../lib/planValidator.cjs');
|
|
11
|
+
|
|
12
|
+
test('true when a pending validator row depends on the job', () => {
|
|
13
|
+
const job = { slug: '111-alpha' };
|
|
14
|
+
const jobs = [
|
|
15
|
+
job,
|
|
16
|
+
{ slug: '999-validate', agentType: 'validator', status: 'pending', dependsOn: ['111-alpha'] },
|
|
17
|
+
];
|
|
18
|
+
expect(hasDownstreamValidator(job, jobs)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('true when the depending validator row is running', () => {
|
|
22
|
+
const job = { slug: '111-alpha' };
|
|
23
|
+
const jobs = [
|
|
24
|
+
job,
|
|
25
|
+
{ slug: '999-validate', agentType: 'validator', status: 'running', dependsOn: ['111-alpha'] },
|
|
26
|
+
];
|
|
27
|
+
expect(hasDownstreamValidator(job, jobs)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('false when the job itself is a validator', () => {
|
|
31
|
+
const job = { slug: '999-validate', agentType: 'validator' };
|
|
32
|
+
const jobs = [
|
|
33
|
+
job,
|
|
34
|
+
{ slug: '998-other-validate', agentType: 'validator', status: 'pending', dependsOn: ['999-validate'] },
|
|
35
|
+
];
|
|
36
|
+
expect(hasDownstreamValidator(job, jobs)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('false for null/undefined job or jobs', () => {
|
|
40
|
+
expect(hasDownstreamValidator(null, [{ slug: 'x' }])).toBe(false);
|
|
41
|
+
expect(hasDownstreamValidator(undefined, [{ slug: 'x' }])).toBe(false);
|
|
42
|
+
expect(hasDownstreamValidator({ slug: '111-alpha' }, null)).toBe(false);
|
|
43
|
+
expect(hasDownstreamValidator({ slug: '111-alpha' }, undefined)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('false when the validator row does not depend on this job', () => {
|
|
47
|
+
const job = { slug: '111-alpha' };
|
|
48
|
+
const jobs = [
|
|
49
|
+
job,
|
|
50
|
+
{ slug: '999-validate', agentType: 'validator', status: 'pending', dependsOn: ['222-beta'] },
|
|
51
|
+
];
|
|
52
|
+
expect(hasDownstreamValidator(job, jobs)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('false when the depending row is a completed validator', () => {
|
|
56
|
+
const job = { slug: '111-alpha' };
|
|
57
|
+
const jobs = [
|
|
58
|
+
job,
|
|
59
|
+
{ slug: '999-validate', agentType: 'validator', status: 'completed', dependsOn: ['111-alpha'] },
|
|
60
|
+
];
|
|
61
|
+
expect(hasDownstreamValidator(job, jobs)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('false when the depending row is not agentType validator', () => {
|
|
65
|
+
const job = { slug: '111-alpha' };
|
|
66
|
+
const jobs = [
|
|
67
|
+
job,
|
|
68
|
+
{ slug: '999-other', agentType: 'dev-lead', status: 'pending', dependsOn: ['111-alpha'] },
|
|
69
|
+
];
|
|
70
|
+
expect(hasDownstreamValidator(job, jobs)).toBe(false);
|
|
71
|
+
});
|
|
@@ -74,7 +74,7 @@ test('buildPrdBody emits required frontmatter keys and body sections in order',
|
|
|
74
74
|
expect(body).toMatch(/- not this/);
|
|
75
75
|
// must point at STANDARDS_PATH rather than inline the standards.md content
|
|
76
76
|
expect(body.includes(STANDARDS_PATH)).toBeTruthy();
|
|
77
|
-
expect(body).toMatch(/
|
|
77
|
+
expect(body).toMatch(/Your system prompt carries the ordered run contract/);
|
|
78
78
|
// the pointer block mentions "Execution discipline" by name, but must not
|
|
79
79
|
// inline the full standards.md prose (e.g. its Performance-section rules)
|
|
80
80
|
expect(body.includes('Lay out hot data contiguously')).toBeFalsy();
|
|
@@ -417,6 +417,8 @@ test('createPrd with a valid payload allocates a group, writes through remote.wr
|
|
|
417
417
|
expect(result.ok).toBe(true);
|
|
418
418
|
expect(typeof result.nn).toBe('number');
|
|
419
419
|
expect(result.filename).toMatch(/^\d+-add-widget-frobnication\.md$/);
|
|
420
|
+
// PRD 1403: sizing warnings ride along in the success response, advisory only.
|
|
421
|
+
expect(Array.isArray(result.warnings)).toBe(true);
|
|
420
422
|
|
|
421
423
|
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
422
424
|
expect(written).toMatch(/^---\n/);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdSizing.test.cjs — unit tests for sizingWarnings (PRD 1403).
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/prdSizing.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect } from 'vitest';
|
|
10
|
+
const { SIZING_LIMITS, sizingWarnings } = require('../lib/prdSizing.cjs');
|
|
11
|
+
|
|
12
|
+
function compliantInput(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
estimateMinutes: 10,
|
|
15
|
+
acceptanceCriteria: ['widget frobnicates on click', 'unit test covers the frobnication path'],
|
|
16
|
+
goal: 'Add frobnication to the widget subsystem.',
|
|
17
|
+
implementationNotes: 'See src/widget.cjs:10.',
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('compliant PRD yields no warnings', () => {
|
|
23
|
+
expect(sizingWarnings(compliantInput())).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('warns when estimateMinutes exceeds the limit', () => {
|
|
27
|
+
const warnings = sizingWarnings(compliantInput({ estimateMinutes: SIZING_LIMITS.estimateMinutes + 1 }));
|
|
28
|
+
expect(warnings.some((w) => /estimateMinutes/.test(w))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('does not warn when estimateMinutes is exactly the limit', () => {
|
|
32
|
+
const warnings = sizingWarnings(compliantInput({ estimateMinutes: SIZING_LIMITS.estimateMinutes }));
|
|
33
|
+
expect(warnings.some((w) => /estimateMinutes/.test(w))).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('warns when acceptanceCriteria has more than the limit of lines', () => {
|
|
37
|
+
const acceptanceCriteria = Array.from({ length: SIZING_LIMITS.acLines + 1 }, (_, i) => `criterion ${i}`);
|
|
38
|
+
const warnings = sizingWarnings(compliantInput({ acceptanceCriteria }));
|
|
39
|
+
expect(warnings.some((w) => /acceptanceCriteria has/.test(w))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('does not warn when acceptanceCriteria has exactly the limit of lines', () => {
|
|
43
|
+
const acceptanceCriteria = Array.from({ length: SIZING_LIMITS.acLines }, (_, i) => `criterion ${i}`);
|
|
44
|
+
const warnings = sizingWarnings(compliantInput({ acceptanceCriteria }));
|
|
45
|
+
expect(warnings.some((w) => /acceptanceCriteria has/.test(w))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('warns when an AC line exceeds the per-line character limit', () => {
|
|
49
|
+
const longLine = 'x'.repeat(SIZING_LIMITS.acLineChars + 1);
|
|
50
|
+
const warnings = sizingWarnings(compliantInput({ acceptanceCriteria: [longLine] }));
|
|
51
|
+
expect(warnings.some((w) => /exceed \d+ characters/.test(w))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('does not warn when an AC line is exactly at the per-line character limit', () => {
|
|
55
|
+
const line = 'x'.repeat(SIZING_LIMITS.acLineChars);
|
|
56
|
+
const warnings = sizingWarnings(compliantInput({ acceptanceCriteria: [line] }));
|
|
57
|
+
expect(warnings.some((w) => /exceed \d+ characters/.test(w))).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('warns on an open-ended search-and-fix AC line', () => {
|
|
61
|
+
const warnings = sizingWarnings(compliantInput({
|
|
62
|
+
acceptanceCriteria: ['grep e2e specs and update expectations to match the new sort order'],
|
|
63
|
+
}));
|
|
64
|
+
expect(warnings.some((w) => /open-ended search-and-fix/.test(w))).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('does not warn on an AC line that merely mentions grep or update in isolation', () => {
|
|
68
|
+
const warnings = sizingWarnings(compliantInput({
|
|
69
|
+
acceptanceCriteria: ['grep confirms no leftover references to the old flag'],
|
|
70
|
+
}));
|
|
71
|
+
expect(warnings.some((w) => /open-ended search-and-fix/.test(w))).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('warns on an open-ended search-and-fix AC line whose "then"/"update" half is a second sentence', () => {
|
|
75
|
+
const warnings = sizingWarnings(compliantInput({
|
|
76
|
+
acceptanceCriteria: ['Grep the repo for every usage of the old flag. Then update each call site.'],
|
|
77
|
+
}));
|
|
78
|
+
expect(warnings.some((w) => /open-ended search-and-fix/.test(w))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('warns on an AC line with two or more timeout commands', () => {
|
|
82
|
+
const warnings = sizingWarnings(compliantInput({
|
|
83
|
+
acceptanceCriteria: ['timeout 120 npm run typecheck && timeout 300 npm run test:unit both pass'],
|
|
84
|
+
}));
|
|
85
|
+
expect(warnings.some((w) => /two or more `timeout/.test(w))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('does not warn on an AC line with a single timeout command', () => {
|
|
89
|
+
const warnings = sizingWarnings(compliantInput({
|
|
90
|
+
acceptanceCriteria: ['timeout 120 npm run typecheck passes'],
|
|
91
|
+
}));
|
|
92
|
+
expect(warnings.some((w) => /two or more `timeout/.test(w))).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('warns when goal + implementationNotes + joined AC exceeds the body character limit', () => {
|
|
96
|
+
const warnings = sizingWarnings(compliantInput({
|
|
97
|
+
goal: 'g'.repeat(SIZING_LIMITS.bodyChars + 1),
|
|
98
|
+
}));
|
|
99
|
+
expect(warnings.some((w) => /exceeding the \d+-character sizing limit/.test(w))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('does not warn when the combined body is exactly at the character limit', () => {
|
|
103
|
+
const goal = 'g'.repeat(SIZING_LIMITS.bodyChars);
|
|
104
|
+
const warnings = sizingWarnings({ estimateMinutes: 10, acceptanceCriteria: [], goal, implementationNotes: '' });
|
|
105
|
+
expect(warnings.some((w) => /exceeding the \d+-character sizing limit/.test(w))).toBe(false);
|
|
106
|
+
});
|
|
@@ -216,6 +216,52 @@ test('rows held by a launch breaker / lease read blocked, not starved (per proje
|
|
|
216
216
|
expect(byProject.map((v) => v.kind)).toEqual(['blocked']);
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
test('a starved queue forces its tick with bypassLoadGate:true — the CPU load gate must not freeze the last-resort dispatch', async () => {
|
|
220
|
+
// Regression for the 2026-09-23 freeze: 3 dispatchable jobs, 0 running,
|
|
221
|
+
// lastTickReason 'load-deferred' unbroken 07:00→13:06. The watchdog is the
|
|
222
|
+
// safety net for exactly that state, but it used to force its tick with
|
|
223
|
+
// bypassLoadGate:false, so a box loaded by ANOTHER process held the net's
|
|
224
|
+
// own tick behind the load gate and nothing ever ran. Spy the exported
|
|
225
|
+
// tickQueue seam (the call is routed via module.exports precisely so this
|
|
226
|
+
// is assertable without manufacturing real machine load, which
|
|
227
|
+
// SM_LOAD_GATE_PER_CORE clamps at 0.25 and can't force deterministically).
|
|
228
|
+
const cwd = '/tmp/sm-starve-bypass-project';
|
|
229
|
+
const jobs = [{ slug: 'bypass-a', cwd, status: 'pending', dependsOn: [] }];
|
|
230
|
+
const state = { jobs, paused: null, lastRunAt: new Date(NOW - 40 * 60_000).toISOString() };
|
|
231
|
+
const opts = { now: NOW, bootedAtMs: NOW - 3 * 60 * 60_000 };
|
|
232
|
+
const tickSpy = vi.spyOn(scheduler, 'tickQueue').mockResolvedValue({ fired: false, reason: 'drained' });
|
|
233
|
+
try {
|
|
234
|
+
const verdict = await scheduler.runQueueStarvationWatchdog(state, opts);
|
|
235
|
+
expect(verdict.kind).toBe('starved');
|
|
236
|
+
expect(tickSpy).toHaveBeenCalledTimes(1);
|
|
237
|
+
expect(tickSpy).toHaveBeenCalledWith({ bypassLoadGate: true });
|
|
238
|
+
} finally {
|
|
239
|
+
tickSpy.mockRestore();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('the bypass is not unconditional — a queue with something already running never force-ticks at all', async () => {
|
|
244
|
+
// Guard against the bypass becoming an escape hatch: the watchdog only
|
|
245
|
+
// reaches its forced tick on a genuine 'starved' verdict (0 running). When
|
|
246
|
+
// a job is already running, classifyQueueStarvation returns null and no
|
|
247
|
+
// forced tick fires — so bypassLoadGate never even comes into play.
|
|
248
|
+
const cwd = '/tmp/sm-running-nobypass-project';
|
|
249
|
+
const jobs = [
|
|
250
|
+
{ slug: 'run-a', cwd, status: 'running', dependsOn: [] },
|
|
251
|
+
{ slug: 'pend-b', cwd, status: 'pending', dependsOn: [] },
|
|
252
|
+
];
|
|
253
|
+
const state = { jobs, paused: null, lastRunAt: new Date(NOW - 40 * 60_000).toISOString() };
|
|
254
|
+
const opts = { now: NOW, bootedAtMs: NOW - 3 * 60 * 60_000 };
|
|
255
|
+
const tickSpy = vi.spyOn(scheduler, 'tickQueue').mockResolvedValue({ fired: false, reason: 'drained' });
|
|
256
|
+
try {
|
|
257
|
+
const verdict = await scheduler.runQueueStarvationWatchdog(state, opts);
|
|
258
|
+
expect(verdict).toBeNull();
|
|
259
|
+
expect(tickSpy).not.toHaveBeenCalled();
|
|
260
|
+
} finally {
|
|
261
|
+
tickSpy.mockRestore();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
219
265
|
test('the watchdog latches: one forced tick / audit per starve episode, re-armed by threshold or running-count change', async () => {
|
|
220
266
|
const cwd = '/tmp/sm-starve-latch-project';
|
|
221
267
|
const jobs = [{ slug: 'latch-a', cwd, status: 'pending', dependsOn: [] }];
|
|
@@ -33,6 +33,7 @@ const path = require('node:path');
|
|
|
33
33
|
let tmpHome;
|
|
34
34
|
let originalHome;
|
|
35
35
|
let scheduler;
|
|
36
|
+
let bustCwdCache;
|
|
36
37
|
|
|
37
38
|
beforeAll(() => {
|
|
38
39
|
originalHome = process.env.HOME;
|
|
@@ -40,6 +41,7 @@ beforeAll(() => {
|
|
|
40
41
|
process.env.HOME = tmpHome;
|
|
41
42
|
|
|
42
43
|
scheduler = require('../scheduler.cjs');
|
|
44
|
+
({ bustCwdCache } = require('../lib/queueStore.cjs'));
|
|
43
45
|
|
|
44
46
|
if (!scheduler.PRDS_DIR.startsWith(tmpHome)) {
|
|
45
47
|
throw new Error(`refusing to run: PRDS_DIR (${scheduler.PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
|
|
@@ -56,6 +58,14 @@ function registerActiveProject(cwd) {
|
|
|
56
58
|
const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
|
|
57
59
|
fs.mkdirSync(slugDir, { recursive: true });
|
|
58
60
|
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
61
|
+
// Each test registers a NEW project mid-file; without busting queueStore's
|
|
62
|
+
// stateCwds cache (CACHE_MS 120s), a reconcile() call from an EARLIER test
|
|
63
|
+
// in this file can leave a stale cwd list cached that this test's freshly
|
|
64
|
+
// registered project is invisible to — reconcile() then never discovers
|
|
65
|
+
// its PRD at all (confirmed: this is what made
|
|
66
|
+
// 'reconcile() still onboards a PRD written to the canonical Epic-scoped
|
|
67
|
+
// dir' intermittently fail under full-suite load, PRD 1414).
|
|
68
|
+
bustCwdCache();
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
function makeFixtureProject(prefix) {
|
|
@@ -32,6 +32,23 @@ const { execFileSync } = require('node:child_process');
|
|
|
32
32
|
|
|
33
33
|
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'looks-done-test-'));
|
|
34
34
|
process.env.HOME = tmpHome;
|
|
35
|
+
// reverifyNeedsReview's own auto-fix pass (PRD 1414 root cause): a
|
|
36
|
+
// needs_review row with autoFixAttempted:true is still RETRY-eligible
|
|
37
|
+
// (selectAutoFixTargets) whenever autoFixOutcome is unset/'no-plan'/'error'
|
|
38
|
+
// and autoFixRetries < 1 — exactly this file's PRD-1136 fixture. reverify
|
|
39
|
+
// then calls spawnInvestigation(job, ...) FIRE-AND-FORGET (no `await`, only
|
|
40
|
+
// `.catch()`), so it keeps running real disk/git I/O in the background after
|
|
41
|
+
// reverifyNeedsReview() (and the test awaiting it) returns. Under
|
|
42
|
+
// `npm run test:unit`'s full-suite load that background work sometimes had
|
|
43
|
+
// enough real time to run before this file's own assertions/next test read
|
|
44
|
+
// queue.json, and was observed re-discovering '30-example' as an unstamped
|
|
45
|
+
// PRD and quarantining it out from under this test (confirmed via a captured
|
|
46
|
+
// failure: 'quarantining unstamped PRD 30-example ... no createdVia
|
|
47
|
+
// provenance'). This file's own tests only assert reverifyNeedsReview's
|
|
48
|
+
// direct return value / queue mutations, never spawnInvestigation's, so
|
|
49
|
+
// disable auto-fix the same way sibling scheduler test files already do
|
|
50
|
+
// (e.g. scheduler-shard-quarantine.test.cjs).
|
|
51
|
+
process.env.SM_AUTOFIX_DISABLE = '1';
|
|
35
52
|
|
|
36
53
|
const { reverifyNeedsReview, computeLooksDone, applyNeedsReviewAutoResolve, findSatisfyingCommitOnMain } = require('../scheduler.cjs');
|
|
37
54
|
const { resolveEpicPrdWriteDir } = require('../lib/prdLocations.cjs');
|
|
@@ -122,6 +139,29 @@ async function wait(ms) {
|
|
|
122
139
|
await new Promise((r) => setTimeout(r, ms));
|
|
123
140
|
}
|
|
124
141
|
|
|
142
|
+
// Every test in this file shares ONE tmpHome (see the module-level mkdtempSync
|
|
143
|
+
// above) and most call reverifyNeedsReview(), which scans EVERY project
|
|
144
|
+
// registered under tmpHome/.claude/projects — not just the one this test just
|
|
145
|
+
// wrote (queueStore.cjs's stateCwds unions allProjectCwds()+activeProjectCwds()
|
|
146
|
+
// across the whole HOME). Without this cleanup, each later test's
|
|
147
|
+
// reverifyNeedsReview() call re-processes every earlier test's already-settled
|
|
148
|
+
// fixture row too (confirmed via full-suite stdout: the PRD-1136 test's own
|
|
149
|
+
// reverifyNeedsReview() call was observed re-running auto-fix bookkeeping for
|
|
150
|
+
// 171-example, a prior test's row), and each re-processed candidate costs 1+
|
|
151
|
+
// real `git` subprocess calls (fetchAllRefs, committedInWindow, ...). That
|
|
152
|
+
// backlog only grows as more tests run, so a later test's single
|
|
153
|
+
// reverifyNeedsReview() call does correspondingly more child-process I/O — on
|
|
154
|
+
// a loaded `npm run test:unit` worker pool this cumulative latency is what
|
|
155
|
+
// occasionally pushes a later test toward vitest's 15s per-test budget
|
|
156
|
+
// (vitest.config.ts documents the same class of load-timeout elsewhere),
|
|
157
|
+
// while the file passes in ~10s alone with nothing to accumulate. Wiping the
|
|
158
|
+
// registry after each test bounds every reverifyNeedsReview() call to just
|
|
159
|
+
// that test's own row(s), removing the cross-test amplification.
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
fs.rmSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true, force: true });
|
|
162
|
+
bustCwdCache();
|
|
163
|
+
});
|
|
164
|
+
|
|
125
165
|
test('failed + no result event (unverified-shaped) + later commit touching declared path → looksDone, transitions to needs_review', async () => {
|
|
126
166
|
const projectCwd = path.join(tmpHome, 'proj-171');
|
|
127
167
|
initRepo(projectCwd);
|
|
@@ -260,3 +260,84 @@ test('PRD 985: a PRD missing entirely still routes via the job row\'s own epicId
|
|
|
260
260
|
);
|
|
261
261
|
expect(appendTranscriptTurn).toHaveBeenCalledWith(cwd, 'psess-985-from-queue-row', expect.anything());
|
|
262
262
|
});
|
|
263
|
+
|
|
264
|
+
// ─── PRD 1407: validator jobs stamp verdicts onto the authoring Epic ────────
|
|
265
|
+
// A completed validator job's result text carries one `VALIDATION: <slug>
|
|
266
|
+
// VERIFIED|REFUTED` line per PRD it checked. Each parsed verdict becomes its
|
|
267
|
+
// own response event BEFORE the validator's own (still-unvalidated)
|
|
268
|
+
// check-in, and the validator's check-in never enqueues a validation prompt
|
|
269
|
+
// for itself.
|
|
270
|
+
test('PRD 1407: a validator job with two sentinel lines appends two verdict events plus its own check-in, and never enqueues validation', async () => {
|
|
271
|
+
const sendPrompt = vi.fn();
|
|
272
|
+
const appendResponseEvent = vi.fn(async () => true);
|
|
273
|
+
const appendTranscriptTurn = vi.fn(async () => {});
|
|
274
|
+
const enqueueValidation = vi.fn();
|
|
275
|
+
const parsePrdRaw = vi.fn(async () => ({ sourcePromptId: 'psess-validator-1' }));
|
|
276
|
+
const loadSessions = vi.fn(async () => ({ tabs: [] }));
|
|
277
|
+
const readResultFromLog = vi.fn(() => [
|
|
278
|
+
'VALIDATION: 111-alpha VERIFIED',
|
|
279
|
+
'VALIDATION: 222-beta REFUTED — missing coverage',
|
|
280
|
+
].join('\n'));
|
|
281
|
+
|
|
282
|
+
await notifyOriginatingTab(
|
|
283
|
+
{ slug: '1407-validator-run', status: 'completed', cwd: '/some/cwd', agentType: 'validator' },
|
|
284
|
+
{
|
|
285
|
+
parsePrdRaw, loadSessions, sendPrompt, appendResponseEvent,
|
|
286
|
+
appendTranscriptTurn, readResultFromLog, enqueueValidation,
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(appendResponseEvent).toHaveBeenCalledTimes(3);
|
|
291
|
+
expect(appendResponseEvent).toHaveBeenNthCalledWith(
|
|
292
|
+
1, '/some/cwd', 'psess-validator-1', expect.stringContaining('111-alpha'),
|
|
293
|
+
{ prdSlug: '111-alpha', outcome: 'completed', validation: 'verified' },
|
|
294
|
+
);
|
|
295
|
+
expect(appendResponseEvent).toHaveBeenNthCalledWith(
|
|
296
|
+
2, '/some/cwd', 'psess-validator-1', expect.stringContaining('222-beta'),
|
|
297
|
+
{ prdSlug: '222-beta', outcome: 'completed', validation: 'refuted' },
|
|
298
|
+
);
|
|
299
|
+
expect(appendResponseEvent).toHaveBeenNthCalledWith(
|
|
300
|
+
3, '/some/cwd', 'psess-validator-1', expect.stringContaining('1407-validator-run'),
|
|
301
|
+
{ prdSlug: '1407-validator-run', outcome: 'completed', validation: 'unvalidated' },
|
|
302
|
+
);
|
|
303
|
+
expect(enqueueValidation).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ─── PRD 1408: a downstream validator suppresses the per-PRD validation ask ─
|
|
307
|
+
test('skips enqueueValidation when loadJobs reports a pending validator row depending on this job', async () => {
|
|
308
|
+
const sendPrompt = vi.fn();
|
|
309
|
+
const appendResponseEvent = vi.fn(async () => true);
|
|
310
|
+
const enqueueValidation = vi.fn();
|
|
311
|
+
const parsePrdRaw = vi.fn(async () => ({ sourcePromptId: 'psess-1' }));
|
|
312
|
+
const loadSessions = vi.fn(async () => ({ tabs: [] }));
|
|
313
|
+
const loadJobs = vi.fn(async () => ([
|
|
314
|
+
{ slug: '1408-work-item' },
|
|
315
|
+
{ slug: '1408-validate', agentType: 'validator', status: 'pending', dependsOn: ['1408-work-item'] },
|
|
316
|
+
]));
|
|
317
|
+
|
|
318
|
+
await notifyOriginatingTab(
|
|
319
|
+
{ slug: '1408-work-item', status: 'completed', cwd: '/some/cwd' },
|
|
320
|
+
{ parsePrdRaw, loadSessions, sendPrompt, appendResponseEvent, enqueueValidation, loadJobs },
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(appendResponseEvent).toHaveBeenCalledTimes(1);
|
|
324
|
+
expect(loadJobs).toHaveBeenCalledTimes(1);
|
|
325
|
+
expect(enqueueValidation).not.toHaveBeenCalled();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('still enqueues validation when loadJobs reports no downstream validator', async () => {
|
|
329
|
+
const sendPrompt = vi.fn();
|
|
330
|
+
const appendResponseEvent = vi.fn(async () => true);
|
|
331
|
+
const enqueueValidation = vi.fn();
|
|
332
|
+
const parsePrdRaw = vi.fn(async () => ({ sourcePromptId: 'psess-1' }));
|
|
333
|
+
const loadSessions = vi.fn(async () => ({ tabs: [] }));
|
|
334
|
+
const loadJobs = vi.fn(async () => ([{ slug: '1408-work-item' }]));
|
|
335
|
+
|
|
336
|
+
await notifyOriginatingTab(
|
|
337
|
+
{ slug: '1408-work-item', status: 'completed', cwd: '/some/cwd' },
|
|
338
|
+
{ parsePrdRaw, loadSessions, sendPrompt, appendResponseEvent, enqueueValidation, loadJobs },
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(appendResponseEvent).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(enqueueValidation).toHaveBeenCalledTimes(1);
|
|
343
|
+
});
|
|
@@ -90,11 +90,16 @@ test('tickQueue dispatches the healthy project while the torn project is skipped
|
|
|
90
90
|
const snaps = fs.readdirSync(path.dirname(torn.queueFile)).filter((f) => f.startsWith('queue.json.corrupt-'));
|
|
91
91
|
expect(snaps).toHaveLength(1);
|
|
92
92
|
|
|
93
|
+
// 60s, not 20s: under npm run test:unit's full-suite parallel-worker load
|
|
94
|
+
// this occasionally needed more than 20s for the real child-process
|
|
95
|
+
// dispatch to land (PRD 1414 — confirmed flaky only inside the full suite,
|
|
96
|
+
// never in isolation or under synthetic single-file CPU load). The
|
|
97
|
+
// surrounding vitest test timeout below (120s) already budgets for this.
|
|
93
98
|
await new Promise((resolve, reject) => {
|
|
94
99
|
const t0 = Date.now();
|
|
95
100
|
const poll = () => {
|
|
96
101
|
if (fs.existsSync(path.join(good.cwd, 'ran-here.marker'))) return resolve();
|
|
97
|
-
if (Date.now() - t0 >
|
|
102
|
+
if (Date.now() - t0 > 60_000) return reject(new Error('healthy project never dispatched'));
|
|
98
103
|
setTimeout(poll, 100);
|
|
99
104
|
};
|
|
100
105
|
poll();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedulerStateSidecarRestore.test.cjs — covers PRD 1392: scheduler-state.json
|
|
3
|
+
* previously left lastPollOk, lastFailureKind, backoffNextAt, cachedUtilization and
|
|
4
|
+
* cachedBindingWindowName memory-only, so a fresh boot showed misleading footer/KPI
|
|
5
|
+
* placeholders (a healthy poller reading "failed", a still-rate-limited state
|
|
6
|
+
* un-showable, utilization stuck at "—") until the next post-boot billing poll
|
|
7
|
+
* overwrote them. Proves loadSchedulerState() restores all five from disk instead of
|
|
8
|
+
* leaving the hardcoded defaults (false / null / null / null / null) in place.
|
|
9
|
+
*
|
|
10
|
+
* scheduler.cjs is a plain CJS require (like rateLimitPollerStreak.test.cjs's), so its
|
|
11
|
+
* module-level `let`s are a singleton for this test file's process — the first test
|
|
12
|
+
* below captures the pre-load snapshot itself (the hardcoded defaults) rather than
|
|
13
|
+
* assuming a fresh module per test, so the proof doesn't depend on test ordering.
|
|
14
|
+
*
|
|
15
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/schedulerStateSidecarRestore.test.cjs
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
import { test, expect, beforeEach } from 'vitest';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
const { loadSchedulerState, persistSchedulerState, getSchedulerStateSnapshot } = require('../scheduler.cjs');
|
|
25
|
+
const schedulerPaths = require('../lib/schedulerPaths.cjs');
|
|
26
|
+
|
|
27
|
+
function writeSidecar(data) {
|
|
28
|
+
fs.mkdirSync(path.dirname(schedulerPaths.schedulerStatePath()), { recursive: true });
|
|
29
|
+
fs.writeFileSync(schedulerPaths.schedulerStatePath(), JSON.stringify(data));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
try { fs.unlinkSync(schedulerPaths.schedulerStatePath()); } catch { /* no sidecar yet */ }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('loadSchedulerState restores lastPollOk/lastFailureKind/backoffNextAt/cachedUtilization/cachedBindingWindowName from the sidecar, not the hardcoded defaults', () => {
|
|
37
|
+
// Before a restore, the module starts (or was left, by whatever ran before this test
|
|
38
|
+
// in-process) at whatever it was — capture it so the assertion below is a genuine
|
|
39
|
+
// "changed to match the file" proof, not an assumption about fresh-module defaults.
|
|
40
|
+
const before = getSchedulerStateSnapshot();
|
|
41
|
+
|
|
42
|
+
const written = {
|
|
43
|
+
version: 1,
|
|
44
|
+
lastPollOk: true,
|
|
45
|
+
lastFailureKind: 'meter_rate_limited',
|
|
46
|
+
backoffNextAt: 1_726_000_000_000,
|
|
47
|
+
cachedUtilization: 42,
|
|
48
|
+
cachedBindingWindowName: 'five_hour',
|
|
49
|
+
};
|
|
50
|
+
writeSidecar(written);
|
|
51
|
+
|
|
52
|
+
// Simulate a restart: a fresh process starts from the hardcoded module defaults
|
|
53
|
+
// (false / null / null / null / null) until loadSchedulerState() runs.
|
|
54
|
+
loadSchedulerState();
|
|
55
|
+
|
|
56
|
+
const after = getSchedulerStateSnapshot();
|
|
57
|
+
expect(after).toEqual({
|
|
58
|
+
lastPollOk: true,
|
|
59
|
+
lastFailureKind: 'meter_rate_limited',
|
|
60
|
+
backoffNextAt: 1_726_000_000_000,
|
|
61
|
+
cachedUtilization: 42,
|
|
62
|
+
cachedBindingWindowName: 'five_hour',
|
|
63
|
+
});
|
|
64
|
+
// The restore must have actually changed something relative to the pre-load
|
|
65
|
+
// snapshot — proves this run exercised the restore path, not a no-op.
|
|
66
|
+
expect(after).not.toEqual(before);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('persistSchedulerState writes the same five fields back out, round-tripping through the sidecar', () => {
|
|
70
|
+
writeSidecar({
|
|
71
|
+
version: 1,
|
|
72
|
+
lastPollOk: false,
|
|
73
|
+
lastFailureKind: 'auth',
|
|
74
|
+
backoffNextAt: 1_726_100_000_000,
|
|
75
|
+
cachedUtilization: 87.5,
|
|
76
|
+
cachedBindingWindowName: 'weekly_all',
|
|
77
|
+
});
|
|
78
|
+
loadSchedulerState();
|
|
79
|
+
|
|
80
|
+
persistSchedulerState();
|
|
81
|
+
|
|
82
|
+
const onDisk = JSON.parse(fs.readFileSync(schedulerPaths.schedulerStatePath(), 'utf8'));
|
|
83
|
+
expect(onDisk.lastPollOk).toBe(false);
|
|
84
|
+
expect(onDisk.lastFailureKind).toBe('auth');
|
|
85
|
+
expect(onDisk.backoffNextAt).toBe(1_726_100_000_000);
|
|
86
|
+
expect(onDisk.cachedUtilization).toBe(87.5);
|
|
87
|
+
expect(onDisk.cachedBindingWindowName).toBe('weekly_all');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('loadSchedulerState against a sidecar missing the five fields leaves the in-memory values unchanged (pre-PRD-1392 file)', () => {
|
|
91
|
+
// Establish a known non-default in-memory state first.
|
|
92
|
+
writeSidecar({
|
|
93
|
+
version: 1,
|
|
94
|
+
lastPollOk: true,
|
|
95
|
+
lastFailureKind: 'transient',
|
|
96
|
+
backoffNextAt: 5000,
|
|
97
|
+
cachedUtilization: 10,
|
|
98
|
+
cachedBindingWindowName: 'seven_day',
|
|
99
|
+
});
|
|
100
|
+
loadSchedulerState();
|
|
101
|
+
const established = getSchedulerStateSnapshot();
|
|
102
|
+
|
|
103
|
+
// A sidecar written before PRD 1392 (or first-boot with only the older fields) has
|
|
104
|
+
// none of the five new keys — loadSchedulerState's per-field `typeof` guards must
|
|
105
|
+
// leave whatever was already in memory alone, not stomp it back to a hardcoded default.
|
|
106
|
+
writeSidecar({ version: 1, lastPollAt: 123 });
|
|
107
|
+
loadSchedulerState();
|
|
108
|
+
|
|
109
|
+
expect(getSchedulerStateSnapshot()).toEqual(established);
|
|
110
|
+
});
|
|
@@ -54,7 +54,7 @@ function silentLogger() {
|
|
|
54
54
|
return { log: () => {}, warn: () => {} };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const ALL_PERSONAS = ['architect', 'dev-lead', 'project-home-builder'];
|
|
57
|
+
const ALL_PERSONAS = ['architect', 'dev-lead', 'project-home-builder', 'validator'];
|
|
58
58
|
|
|
59
59
|
test('fresh seed writes every persona file', async () => {
|
|
60
60
|
await seedAgentPersonas({ logger: silentLogger() });
|
|
@@ -156,6 +156,24 @@ test('the bundled project-home-builder persona contains no session-manager-repo
|
|
|
156
156
|
}
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
test('seeded dev-lead persona pins an executor-tier model', () => {
|
|
160
|
+
const { splitFrontmatter } = require('../lib/prdFrontmatter.cjs');
|
|
161
|
+
const raw = fs.readFileSync(path.join(__dirname, '..', '..', 'seed', 'agents', 'dev-lead.md'), 'utf8');
|
|
162
|
+
const { fm } = splitFrontmatter(raw);
|
|
163
|
+
expect(fm.model).toBe('sonnet');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('seeded dev-lead body fits AGENT_BODY_CHAR_CAP and carries the run contract', () => {
|
|
167
|
+
const { splitFrontmatter } = require('../lib/prdFrontmatter.cjs');
|
|
168
|
+
const { AGENT_BODY_CHAR_CAP } = require('../lib/agentModelResolve.cjs');
|
|
169
|
+
const raw = fs.readFileSync(path.join(__dirname, '..', '..', 'seed', 'agents', 'dev-lead.md'), 'utf8');
|
|
170
|
+
const { body } = splitFrontmatter(raw);
|
|
171
|
+
const trimmed = body.trim();
|
|
172
|
+
expect(trimmed.length).toBeLessThan(AGENT_BODY_CHAR_CAP);
|
|
173
|
+
expect(trimmed).toContain('## Run contract');
|
|
174
|
+
expect(trimmed).toContain('SCHEDULER_VERDICT: PASS');
|
|
175
|
+
});
|
|
176
|
+
|
|
159
177
|
test('SM_SEED_AGENT_PERSONAS_DISABLE=1 short-circuits', async () => {
|
|
160
178
|
process.env.SM_SEED_AGENT_PERSONAS_DISABLE = '1';
|
|
161
179
|
|